Skip to content

feat(metrics): add public constructors for metric data types#3489

Open
darklight3it wants to merge 5 commits intoopen-telemetry:mainfrom
darklight3it:add-public-constructor-to-metric-data-types
Open

feat(metrics): add public constructors for metric data types#3489
darklight3it wants to merge 5 commits intoopen-telemetry:mainfrom
darklight3it:add-public-constructor-to-metric-data-types

Conversation

@darklight3it
Copy link
Copy Markdown

@darklight3it darklight3it commented May 3, 2026

Fixes #3469

Changes

Added public constructors to metric data types, allowing users to use the public PushMetricExporter::export.

Rationale:

  • Types with one or more optional fields use the builder pattern, where the builder explicitly lists mandatory fields. This matches the established pattern in opentelemetry-sdk, opentelemetry-otlp, and other crates.
  • Types with only required fields use a new method.

Summary:

Type Constructor Required fields Optional (with_*) fields
ResourceMetrics builder() resource, scope_metrics
ScopeMetrics builder() scope, metrics
Metric builder(name, data) name, data description, unit
Gauge<T> builder(data_points, time) data_points, time start_time
GaugeDataPoint<T> builder(value) value attributes, exemplars
Sum<T> new(...) data_points, temporality, is_monotonic, start_time, time
SumDataPoint<T> builder(value) value attributes, exemplars
Histogram<T> new(...) data_points, temporality, start_time, time
HistogramDataPoint<T> builder(count, sum, bounds, bucket_counts) count, sum, bounds, bucket_counts attributes, min, max, exemplars
ExponentialHistogram<T> new(...) data_points, temporality, start_time, time
ExponentialHistogramDataPoint<T> builder(count, sum, scale, zero_count, positive_bucket, negative_bucket) count, sum, scale, zero_count, positive_bucket, negative_bucket attributes, min, max, zero_threshold, exemplars
ExponentialBucket new(offset, counts) offset, counts
Exemplar<T> builder(value, time) value, time filtered_attributes, span_id, trace_id

Example usage:

use opentelemetry::{InstrumentationScope, KeyValue};
use opentelemetry::time::now;
use opentelemetry_sdk::metrics::data::*;
use opentelemetry_sdk::metrics::{Temporality, PushMetricExporter};
use opentelemetry_sdk::Resource;

let gauge_dp = GaugeDataPoint::builder(42.0_f64)
    .with_attributes(vec![KeyValue::new("host", "localhost")])
    .build();

let gauge = Gauge::builder(vec![gauge_dp], now()).build();

let metric = Metric::builder("cpu.usage", AggregatedMetrics::F64(MetricData::Gauge(gauge)))
    .with_description("CPU usage percentage")
    .with_unit("%")
    .build();

let scope_metrics = ScopeMetrics::builder()
    .with_scope(InstrumentationScope::builder("my-library").build())
    .with_metrics(vec![metric])
    .build();

let resource_metrics = ResourceMetrics::builder()
    .with_resource(Resource::builder().with_service_name("my-service").build())
    .with_scope_metrics(vec![scope_metrics])
    .build();

exporter.export(&resource_metrics).await?;

Merge requirement checklist

  • CONTRIBUTING guidelines followed
  • Unit tests added/updated (if applicable)
  • Appropriate CHANGELOG.md files updated for non-trivial, user-facing changes
  • Changes in public API reviewed (if applicable)

@linux-foundation-easycla
Copy link
Copy Markdown

linux-foundation-easycla Bot commented May 3, 2026

CLA Not Signed

@darklight3it darklight3it changed the title Add public constructors for metric data types feat(metrics): add public constructors for metric data types May 3, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 3, 2026

Codecov Report

❌ Patch coverage is 99.08884% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.4%. Comparing base (cef3317) to head (e42bbdc).

Files with missing lines Patch % Lines
opentelemetry-sdk/src/metrics/data/mod.rs 99.0% 4 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##            main   #3489     +/-   ##
=======================================
+ Coverage   83.7%   84.4%   +0.7%     
=======================================
  Files        126     126             
  Lines      25386   25825    +439     
=======================================
+ Hits       21255   21818    +563     
+ Misses      4131    4007    -124     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@darklight3it darklight3it force-pushed the add-public-constructor-to-metric-data-types branch from f3fc113 to d3e1646 Compare May 3, 2026 12:30
@darklight3it darklight3it marked this pull request as ready for review May 5, 2026 09:47
@darklight3it darklight3it requested a review from a team as a code owner May 5, 2026 09:47
@lalitb
Copy link
Copy Markdown
Member

lalitb commented May 6, 2026

@darklight3it - Can you clarify the intended use case for constructing ResourceMetrics and calling PushMetricExporter::export() directly?

In the normal metrics pipeline, instrumentation libraries create measurements through instruments, and the SDK/MetricReader produces ResourceMetrics for the exporter. So the example shared in the desc looks like a lower-level/exporter use case rather than normal instrumentation.

Is the target use case pre-aggregated metrics from an external source, or directly sending measurements to an exporter while bypassing SDK aggregation? If it is the latter, I am not sure ResourceMetrics constructors are the right API shape, since these types represent already-aggregated metric points, not raw measurements. The spec direction for third-party/pre-processed metric sources seems closer to a MetricProducer plugged into a MetricReader.

@darklight3it
Copy link
Copy Markdown
Author

Yes, the target use case is pre-aggregated metrics from an external source. The aggregation is already done, the Rust side only needs to serialize and transport the resulting ResourceMetrics via OTLP.

The challenge I'm running into is that MetricExporter::export() is public and intended to be callable externally, but its argument (ResourceMetrics) can't be constructed outside the crate.

This makes it difficult to use the exporter trait independently of the SDK's internal reader pipeline. I think opening up ResourceMetrics would better support the modularity that motivated having opentelemetry-otlp as a separate crate and would bring metrics in line with spans and logs, where SpanData and LogRecord already have public fields and work well for custom exporter implementations.

@lalitb
Copy link
Copy Markdown
Member

lalitb commented May 7, 2026

Thanks, that use case makes sense.

I think this is closer to the spec’s MetricProducer path, though. MetricProducer is meant for third-party/pre processed metric sources, and it plugs into a MetricReader. So the shape would be more like: external aggregated source -> MetricProducer -> MetricReader -> exporter.

That makes me hesitant to expose constructors for the full ResourceMetrics tree and encourage calling PushMetricExporter::export() directly. It feels like we would be making a lot of SDK metric data internals part of the stable public construction API. I’d prefer we discuss a smaller API for this use case, maybe MetricProducer-style, instead of opening up all these data types as-is.

@darklight3it
Copy link
Copy Markdown
Author

darklight3it commented May 7, 2026

Thanks @lalitb for the thoughtful feedback, I appreciate the discussion.

On MetricProducer:

On the concern about opening up the full ResourceMetrics tree:

  • The builder doesn't expose fields directly. It keeps internal data behind public methods, the same pattern as the existing getters (and in other types throughout the repo). The stability surface is symmetric with what's already committed: export() is public, the getters are public, ResourceMetrics::Default() already allows external construction. The builder just lets you construct a populated instance.

  • The PR does touch nested types (ScopeMetrics, Metric, etc.) with the same builder pattern. But the same argument applies uniformly: each of these types already has public getters that expose their shape. The builders don't add new surface area, they provide the construction counterpart to what's already readable. New fields and new subtypes should already be maintained like this for the reading part.

  • If maintenance is a concern, a derive macro (like typed-builder) could auto-generate the builders from the struct definitions, keeping them in sync automatically. I considered that approach but chose hand-written builders to avoid adding a proc-macro dependency to the crate, which I think is the right choice for a foundational library like this.

@lalitb
Copy link
Copy Markdown
Member

lalitb commented May 8, 2026

I think MetricProducer is still the closer conceptual fit - and it would be better to invest in bringing this feature back.

The “pull” part there is only inside the SDK: the MetricReader asks the producer for already-aggregated data. It does not mean the final export has to be pull-based. The final exporter can still be a push exporter sending OTLP.

So the shape would be: external pre-aggregated source -> MetricProducer -> MetricReader -> PushMetricExporter.

For the immediate-export case, we may need an API that lets the reader collect/export on demand, but I do not think that means PushMetricExporter::export should become the public ingestion path or that we should expose builders for the whole ResourceMetrics tree.

@darklight3it
Copy link
Copy Markdown
Author

darklight3it commented May 9, 2026

I understand the preference for an internal path, but if I'm reading the proposed workflow correctly, the changes in this PR are still necessary for that to work.

// simplified MetricProducer shape to illustrate the point
struct MyProducer { buffer: Mutex<Vec<ScopeMetrics>> }

impl MetricProducer for MyProducer {
    fn produce(&self) -> Vec<ScopeMetrics> {
         self.buffer.lock().unwrap().drain(..).collect()
     }
 }

fn receive_external_metrics(data: ExternalData) {
     let scope_metrics: Vec<ScopeMetrics> = data.into(); // needs constructors for
                                                          // ScopeMetrics, Metric,
                                                          // Sum/Gauge/Histogram,
                                                          // DataPoint
     PRODUCER.buffer.lock().unwrap().extend(scope_metrics);
     READER.force_collect(); // on-demand collect to control when data is flushed
 }

Either way, users will need a supported way to create these types from external data, which means exposing some form of construction contract. We could introduce a parallel set of public DTO types and map them to the ResourceMetrics tree internally, but given that these types are already partially public (getters, Default, trait signatures), creating a separate anticorruption layer won't help without breaking the contract and closing this types.

The same reasoning applies to the PushMetricExporter. It's public but currently unusable since its argument can't be constructed. If the intent is for it not to be called directly, it may be worth reconsidering whether it should remain public in a separate crate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add public constructors for metric data types

2 participants